package com.trg.flex.frm
{
	import com.trg.flex.Callback;
	import com.trg.flex.FlexUtils;
	import com.trg.flex.MetaDataUtils;
	import com.trg.flex.messages.ErrorMessageConvertor;
	import com.trg.flex.messages.Message;
	import com.trg.flex.messages.MessagePanel;
	
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayCollection;
	import mx.containers.VBox;
	import mx.core.UIComponent;
	import mx.events.FlexEvent;
	import mx.events.PropertyChangeEvent;
	import mx.managers.FocusManager;
	import mx.managers.IFocusManager;
	import mx.managers.IFocusManagerComponent;
	
	/**
	 * <p>
	 * A FormContainer is a container that will hold one or more forms.  The FormContainer
	 * uses the specified bean(s) (if any) to populate the inputs.
	 * </p>
	 * 
	 * <code>
	 *  <components:FormContainer bean="{owner}" id="formContainer">
	 *   <mx:Form>
	 * 	  <mx:FormItem>
	 *     <components:CustomTextInput property="name"/>
	 *    </mx:FormItem>
	 *    <mx:Button label="Clear" click="formContainer.clear()"/>
	 *    <mx:Button label="Reset" click="formContainer.reset()"/>
	 *   </mx:Form>
	 *  </components:FormContainer>
	 * </code>
	 */
	
	[Event(name="onCommit",type="com.trg.flex.frm.FormEvent")]
	[Event(name="onCommitComplete",type="com.trg.flex.frm.FormEvent")]
	[Event(name="onCommitFailed",type="com.trg.flex.frm.FormEvent")]
	[Event(name="onBeanChanged",type="com.trg.flex.frm.FormEvent")]
	public class FormContainer extends VBox
	{
		protected const COMMIT_CALLBACK:Callback = new Callback(commitComplete, commitFail);
		
		[Bindable]
		public var committing:Boolean = false;
		[Bindable]
		public var formChanged:Boolean = false;
		[Bindable]
		public var defaultFocus:IFocusManagerComponent;
		
		protected var _formBean:Object;
		protected var _beanType:Class = Object;
		protected var _originalBean:Object;
		private var _inputs:ArrayCollection = new ArrayCollection();
		private var _validators:Dictionary = new Dictionary();
		private var _initialized:Boolean = false;
		private var _valid:Boolean;
		
		private var _messagePanels:Array = [];

		/* PUBLIC METHODS */
		public function get bean() : Object {
			inputsToFormBean();
			var bean:* = new _beanType();
			formBeanToOriginal(null, bean);
			return bean;
		}

		public function set bean(bean : Object) : void {
			_originalBean = bean;
			if (!_initialized) return;
			if (bean == null) {
				clear();	
			} else {
				originalToFormBean();
				formBeanToInputs();
			}
			var event:FormEvent = new FormEvent(FormEvent.BEAN_CHANGED);
			event.formBean = _formBean;
			event.original = _originalBean;
			//resetFocus();
			dispatchEvent(event);
		}
		
		[Bindable(event="onBeanChanged")]
		public function get original() : Object {
			return _originalBean;
		}

		public function set beanType(type:Class):void {
			if (type == null) type = Object;
			_beanType = type;
			_formBean = new _beanType();
		}
		
		public function get beanType():Class {
			return _beanType;
		}
		
		public function clear() : void {
			doBeforeClear(_formBean);
			_formBean = new _beanType();
			for(var i:int = 0; i < _inputs.length; i++) {
				var property:String = _inputs[i].property;
				_inputs[i].elementValue = null;
			}
			doAfterClear(_formBean);
			clearValidation();
			//resetFocus();
		}
		
		public function commit() : void {
			if (!valid || committing) return;
			committing = true;
			inputsToFormBean();
			doValidateOnCommit(_formBean);
			doCommit(_formBean);
			
			var event:FormEvent = new FormEvent(FormEvent.COMMIT);
			event.formBean = _formBean;
			event.original = _originalBean;
			event.callback = COMMIT_CALLBACK;
			dispatchEvent(event);
		}

		public function reset() : void {
			if (_originalBean == null) {
				clear();	
			} else {
				originalToFormBean();
				formBeanToInputs();
			}
			//resetFocus();
		}
		
		public function updateOriginal(formBean:* = null, original:* = null) : void {
			if (original == null) original = this._originalBean;
			if (formBean == null) {
				formBean = this._formBean;
				inputsToFormBean();
			}
			formBeanToOriginal(formBean, original);
		}
		
		public function resetFocus(fm:IFocusManager = null):void {
			var focusComponent:IFocusManagerComponent;
			if (defaultFocus != null) {
				focusComponent = defaultFocus;
			} else {
				focusComponent = FlexUtils.descendantSearch(this, isFocusableComponent) as IFocusManagerComponent;
			}
			
			if (focusComponent) {
				if (!fm) {
					fm = Object(focusComponent).focusManager;
				}
				if (fm) {
					fm.setFocus(focusComponent);
				}
			}
			
			doAfterResetFocus(fm, focusComponent);
			
			clearMessages();
		}
		protected function isFocusableComponent(x:*):Boolean {
			return x is IFocusManagerComponent && x.focusEnabled && _inputs.contains(x);
		}
		
		public function getPropertyType(property:String):Class {
			return MetaDataUtils.getPropertyType(beanType, property);
		}
		
		[Bindable(event="validChanged")]
		public function get valid():Boolean {
			return _valid;
		}
		
		
		protected var _autoMessagePanel:MessagePanel;
		protected function prepareAutoMessagePanel():void {
			if (_autoMessagePanel == null && _messagePanels.length == 0) {
				_autoMessagePanel = new MessagePanel();
				addChildAt(_autoMessagePanel, 0);
			} else if (_autoMessagePanel != null && _messagePanels.length > 1) {
				removeChild(_autoMessagePanel);
				_autoMessagePanel = null;
			}
		}
		
		public function setMessage(msg:Message):void {
			prepareAutoMessagePanel();
			for each (var messagePanel:MessagePanel in _messagePanels) {
				messagePanel.setMessage(msg);
			}
		}
		
		public function addMessage(msg:Message):void {
			prepareAutoMessagePanel();
			for each (var messagePanel:MessagePanel in _messagePanels) {
				messagePanel.addMessage(msg);
			}
		}
		
		public function clearMessages():void {
			prepareAutoMessagePanel();
			for each (var messagePanel:MessagePanel in _messagePanels) {
				messagePanel.clearMessages();
			}
		}
		
		/* INITIALIZATION */
		public function FormContainer() {
			super();
			this.addEventListener(FlexEvent.INITIALIZE, initComplete);
		}

		/**
		 * Called by form elements to add themselves to the FormContainer
		 * when they are added to the UI as descendents of the FormContainer.
		 * This makes the FormContainer aware of the element so it can be used
		 * in the various functions of the FormContainer.
		 */
		public function addElement(element:FormElement):void {
			//only elements with "property" set should be added to the form container
			if (element.property == null) return;
			
			_inputs.addItem(element);
			
			if (beanType != Object && MetaDataUtils.getPropertyType(_beanType, element.property) == null)
				throw new Error("Error building form container: the property ("
					+ element.property + ") is not available for type (" + _beanType + ").");
			
			if (_originalBean != null) {
				setInputValue(_formBean, element);
			}
			
			if (element is FormInput) {
				var input:FormInput = element as FormInput;
				
				input.type = MetaDataUtils.getPropertyType(_beanType, input.property);
				input.setMetaData(MetaDataUtils.getPropertyMetaData(_beanType, input.property));
				
				if (_valid && !input.valid) {
					_valid = false;
					dispatchEvent(new Event("validChanged"));
				}
				input.clearValidation();
				input.addEventListener("validChanged", inputValidChangeHandler);
			}
			
		}
		
		/**
		 * If an element needs to be removed from the form, this must be
		 * called to disassociate it.
		 */
		public function removeElement(input:FormInput):void {
			_inputs.removeItemAt(_inputs.getItemIndex(input));

			//If the form was valid before it will still be valid. If it was invalid
			//before, it might be valid now.
			if (_valid = false) {
				var tempValid:Boolean = true;
				for(var i:int = 0; i < _inputs.length; i++) {
					if (_inputs[i] is FormInput) {
						tempValid &&= FormInput(_inputs[i]).valid;
					}
					_inputs[i].elementValue = null;
				}
				if (tempValid) {
					_valid = tempValid;
					dispatchEvent(new Event("validChanged"));
				}
			}
		}
		
		public function addMessagePanel(mp:MessagePanel):void {
			if (mp == null) return;
			_messagePanels.push(mp);
		}
		
		public function removeMessagePanel(mp:MessagePanel):void {
			var i:int = _messagePanels.indexOf(mp);
			if (i > -1) _messagePanels.splice(i, 1);
		}
		
		private function initComplete(event:FlexEvent):void {
			_initialized = true;
			if (_originalBean != null) bean = _originalBean;
		}
		
		/* VALIDATION */
		/**
		 * Remove error indicators from all inputs.
		 * Refresh valid flag.
		 */
		private function clearValidation():void {
			_valid = false;
			var tempValid:Boolean = true;
			for(var i:int = 0; i < _inputs.length; i++) {
				if (_inputs[i] is FormInput) {
					FormInput(_inputs[i]).clearValidation();
					tempValid &&= FormInput(_inputs[i]).valid;
				}
			}
			_valid = tempValid;
			dispatchEvent(new Event("validChanged"));
		}
		
		/**
		 * This should be called when the valid state of an input changes.
		 * If the input is invalid, mark the form as invalid.
		 * If the input is valid and all other inputs are valid, mark the form as valid.
		 */
		private function inputValidChangeHandler(event:PropertyChangeEvent):void {
			var input:FormInput = event.target as FormInput;
			if (input == null) return;
			
			if (input.valid) {
				if (_valid == false) {
					//if any of the validators are still INVALID keep this.valid false
					for(var i:int = 0; i < _inputs.length; i++) {
						if (_inputs[i] is FormInput && !_inputs[i].valid)
							return;
					}
					//if none were INVALID, set this.valid to true
					_valid = true;
				}		
			} else {
				//Since this one is INVALID, the whole form is marked as this.valid = false
				_valid = false;
			}
			dispatchEvent(new Event("validChanged"));			
		}
		
		/* PRIVATE METHODS */
		private function originalToFormBean(original:* = null, formBean:* = null) : void {
			if (original == null) original = this._originalBean;
			if (formBean == null) formBean = this._formBean;
			doBeforeOriginalToFormBean(original, formBean);
			var o:Object = doCopy(original, formBean);
			if (formBean == null) this._formBean = o;
			doAfterOriginalToFormBean(original, formBean);
		}
		
		private function formBeanToOriginal(formBean:* = null, original:* = null) : void {
			if (formBean == null) formBean = this._formBean;
			if (original == null) original = this._originalBean;
			doBeforeFormBeanToOriginal(formBean, original);
			var o:Object = doCopy(formBean, original);
			if (original == null) this._originalBean = o;
			doAfterFormBeanToOriginal(formBean, original);
		}
		
		private function formBeanToInputs(formBean:* = null) : void {
			if (formBean == null) formBean = this._formBean;
			doBeforeFormBeanToInputs(formBean);
			
			for(var i:int = 0; i < _inputs.length; i++) {
				setInputValue(formBean, _inputs[i] as FormElement);
			}
			
			doAfterFormBeanToInputs(formBean);
			clearValidation();
		}
		
		private function inputsToFormBean(formBean:* = null) : void {
			if (formBean == null) {
				if (this._formBean == null) this._formBean = new _beanType();
				formBean = this._formBean;
			}
			doBeforeInputsToFormBean(formBean);
			
			for(var i:int = 0; i < _inputs.length; i++) {
				if (_inputs[i] is FormInput) {
					getInputValue(formBean, _inputs[i] as FormInput);
				}
			}
			
			doAfterInputsToFormBean(formBean);
		}
		
		private function setInputValue(formBean:*, input:FormElement):void {
			var prop:String = input.property;
			var value:* = FlexUtils.getNestedProperty(formBean, prop);
			input.elementValue = value;
		}
		
		private function getInputValue(formBean:*, input:FormInput):void {
			var prop:String = input.property;
			var value:* = input.elementValue;
			FlexUtils.setNestedProperty(formBean, prop, value, true);
		}
		
		private function commitComplete(data : Object) : void {
			committing = false;
			doCommitComplete(data);
			var event:FormEvent = new FormEvent(FormEvent.COMMIT_COMPLETE);
			event.formBean = _formBean;
			event.original = _originalBean;
			event.data = data;
			dispatchEvent(event);
		}
		
		private function commitFail(error : Error) : void {
			committing = false;
			setMessage(new Message(error.message, Message.TYPE_ERROR));
			
			doCommitFailed(error);
			var event:FormEvent = new FormEvent(FormEvent.COMMIT_FAILED);
			event.formBean = _formBean;
			event.original = _originalBean;
			event.error = error;
			dispatchEvent(event);
		}
		
		
		/* OVERRIDE METHODS */
		protected function doCopy(src:*, dest:*):* {
			return FlexUtils.shallowCopy(src, dest);
		}
		
		protected function doBeforeClear(formBean:*) : void { }
		
		protected function doAfterClear(formBean:*) : void { }
		
		protected function doCommit(formBean:*) : void { }
		
		protected function doCommitComplete(data : Object) : void { }
		
		protected function doCommitFailed(error : Error) : void { }
		
		protected function doBeforeOriginalToFormBean(original:*, formBean:*) : void { }
		
		protected function doAfterOriginalToFormBean(original:*, formBean:*) : void { }
		
		protected function doBeforeFormBeanToOriginal(formBean:*, original:*) : void { }
		
		protected function doAfterFormBeanToOriginal(formBean:*, original:*) : void { }
		
		protected function doBeforeFormBeanToInputs(formBean:*) : void { }
		
		protected function doAfterFormBeanToInputs(formBean:*) : void { }
		
		protected function doBeforeInputsToFormBean(formBean:*) : void { }
		
		protected function doAfterInputsToFormBean(formBean:*) : void { }
		
		protected function doAfterResetFocus(fm:IFocusManager, focusComponent:IFocusManagerComponent):void { }
		
		/**
		 * @return false for failed validation, true for successful validation
		 */
		protected function doValidateOnCommit(formBean:*) : Boolean {
			return true;
		}
		
		/**
		 * @return false for failed validation, true for successful validation
		 */
		protected function doValidateOnChange(source : Object) : Boolean {
			return true;
		}
	}
}